package com.andtinder.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import com.andtinder.model.CardModel;
import com.andtinder.model.Orientations.Orientation;
import com.facebook.drawee.view.SimpleDraweeView;
import com.konradjanica.amatch.MainActivity;
import com.konradjanica.amatch.R;
import java.util.Random;
import me.grantland.widget.AutofitTextView;
public class CardContainer extends AdapterView<ListAdapter> {
public static final int INVALID_POINTER_ID = -1;
private int mActivePointerId = INVALID_POINTER_ID;
private static final double DISORDERED_MAX_ROTATION_RADIANS = Math.PI / 64;
private int mNumberOfCards = -1;
private final DataSetObserver mDataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
super.onChanged();
clearStack();
ensureFull();
}
@Override
public void onInvalidated() {
super.onInvalidated();
clearStack();
}
};
private final Random mRandom = new Random();
private final Rect boundsRect = new Rect();
private final Rect childRect = new Rect();
private final Matrix mMatrix = new Matrix();
private static final int mMaxVisible = 5;
private static final float mMaxRotation = 40; //degrees
private GestureDetector mGestureDetector;
private int mFlingSlop;
private Orientation mOrientation;
private ListAdapter mListAdapter;
private float mLastTouchX;
private float mLastTouchY;
private View mTopCard;
private int mTouchSlop;
private int mGravity;
private int mNextAdapterPosition;
private boolean mDragging;
private boolean mIsFlingAnimating;
private boolean mIsUrlPressedDown;
private boolean mIsRemovedNoFling;
private int mAdapterStartIndex;
public View getTopCardView() {
return mTopCard;
}
public CardContainer(Context context) {
super(context);
setOrientation(Orientation.Disordered);
setGravity(Gravity.CENTER);
init();
}
public CardContainer(Context context, AttributeSet attrs) {
super(context, attrs);
initFromXml(attrs);
init();
}
public CardContainer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initFromXml(attrs);
init();
}
private void init() {
ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
mFlingSlop = viewConfiguration.getScaledMinimumFlingVelocity();
mTouchSlop = viewConfiguration.getScaledTouchSlop();
mGestureDetector = new GestureDetector(getContext(), new GestureListener());
mIsFlingAnimating = false;
mIsUrlPressedDown = false;
mIsRemovedNoFling = false;
mAdapterStartIndex = 0;
}
private void initFromXml(AttributeSet attr) {
TypedArray a = getContext().obtainStyledAttributes(attr,
R.styleable.CardContainer);
setGravity(a.getInteger(R.styleable.CardContainer_android_gravity, Gravity.CENTER));
int orientation = a.getInteger(R.styleable.CardContainer_orientation, 1);
setOrientation(Orientation.fromIndex(orientation));
a.recycle();
}
@Override
public ListAdapter getAdapter() {
return mListAdapter;
}
@Override
public void setAdapter(ListAdapter adapter) {
if (mListAdapter != null)
mListAdapter.unregisterDataSetObserver(mDataSetObserver);
clearStack();
mTopCard = null;
mListAdapter = adapter;
mNextAdapterPosition = 0;
mAdapterStartIndex = 0;
adapter.registerDataSetObserver(mDataSetObserver);
refreshTopCard();
requestLayout();
}
private void ensureFull() {
while (mNextAdapterPosition < mListAdapter.getCount() && getChildCount() < mMaxVisible) {
View view = mListAdapter.getView(mNextAdapterPosition, null, this);
view.setLayerType(LAYER_TYPE_SOFTWARE, null);
if (mOrientation == Orientation.Disordered) {
if (getChildCount() != 0) {
view.setRotation(getDisorderedRotation());
} else {
addUrlListener(view);
}
}
addViewInLayout(view, 0, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
mListAdapter.getItemViewType(mNextAdapterPosition)), false);
requestLayout();
mNextAdapterPosition += 1;
}
}
/**
* Gets the previously destroyed card and returns it to top of stack
*
* @return false if there are no previously destroyed cards
*/
public boolean retrieveLastCard() {
if (mListAdapter.getCount() > 0
&& mAdapterStartIndex > 0) {
mAdapterStartIndex -= 1;
View view = mListAdapter.getView(mAdapterStartIndex, null, this);
view.setLayerType(LAYER_TYPE_SOFTWARE, null);
if (mOrientation == Orientation.Disordered) {
addUrlListener(view);
}
int topIndexInLayout = mNextAdapterPosition - mAdapterStartIndex - 1;
addViewInLayout(view, topIndexInLayout, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
mListAdapter.getItemViewType(mNextAdapterPosition)), false);
requestLayout();
refreshTopCard();
return true;
}
return false;
}
public void clearStack() {
removeAllViewsInLayout();
mNextAdapterPosition = 0;
mTopCard = null;
}
public Orientation getOrientation() {
return mOrientation;
}
public void setOrientation(Orientation orientation) {
if (orientation == null)
throw new NullPointerException("Orientation may not be null");
if (mOrientation != orientation) {
this.mOrientation = orientation;
if (orientation == Orientation.Disordered) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.setRotation(getDisorderedRotation());
}
} else {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.setRotation(0);
}
}
requestLayout();
}
}
private float getDisorderedRotation() {
return (float) Math.toDegrees(mRandom.nextGaussian() * DISORDERED_MAX_ROTATION_RADIANS);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int requestedWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int requestedHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
int childWidth, childHeight;
if (mOrientation == Orientation.Disordered) {
int R1, R2;
if (requestedWidth >= requestedHeight) {
R1 = requestedHeight;
R2 = requestedWidth;
} else {
R1 = requestedWidth;
R2 = requestedHeight;
}
childWidth = (int) ((R1 * Math.cos(DISORDERED_MAX_ROTATION_RADIANS) - R2 * Math.sin(DISORDERED_MAX_ROTATION_RADIANS)) / Math.cos(2 * DISORDERED_MAX_ROTATION_RADIANS));
childHeight = (int) ((R2 * Math.cos(DISORDERED_MAX_ROTATION_RADIANS) - R1 * Math.sin(DISORDERED_MAX_ROTATION_RADIANS)) / Math.cos(2 * DISORDERED_MAX_ROTATION_RADIANS));
} else {
childWidth = requestedWidth;
childHeight = requestedHeight;
}
int childWidthMeasureSpec, childHeightMeasureSpec;
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
assert child != null;
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
for (int i = 0; i < getChildCount(); i++) {
boundsRect.set(0, 0, getWidth(), getHeight());
View view = getChildAt(i);
int w, h;
w = view.getMeasuredWidth();
h = view.getMeasuredHeight();
Gravity.apply(mGravity, w, h, boundsRect, childRect);
view.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mTopCard == null) {
return false;
}
if (mIsUrlPressedDown) {
return false;
}
if (mGestureDetector.onTouchEvent(event)) {
return true;
}
if (removeTopCardRotation()) {
mIsRemovedNoFling = true;
return true;
}
Log.d("Touch Event", MotionEvent.actionToString(event.getActionMasked()) + " ");
final int pointerIndex;
final float x, y;
final float dx, dy;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (!mIsRemovedNoFling && !mIsFlingAnimating) {
mTopCard.getHitRect(childRect);
pointerIndex = event.getActionIndex();
x = event.getX(pointerIndex);
y = event.getY(pointerIndex);
if (!childRect.contains((int) x, (int) y)) {
return false;
}
mLastTouchX = x;
mLastTouchY = y;
mActivePointerId = event.getPointerId(pointerIndex);
float[] points = new float[]{x - mTopCard.getLeft(), y - mTopCard.getTop()};
mTopCard.getMatrix().invert(mMatrix);
mMatrix.mapPoints(points);
mTopCard.setPivotX(points[0]);
mTopCard.setPivotY(points[1]);
}
break;
case MotionEvent.ACTION_MOVE:
if (!mIsRemovedNoFling && !mIsFlingAnimating) {
pointerIndex = event.findPointerIndex(mActivePointerId);
x = event.getX(pointerIndex);
y = event.getY(pointerIndex);
dx = x - mLastTouchX;
dy = y - mLastTouchY;
if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) {
mDragging = true;
}
if (!mDragging) {
return true;
}
mTopCard.setTranslationX(mTopCard.getTranslationX() + dx);
mTopCard.setTranslationY(mTopCard.getTranslationY() + dy);
mTopCard.setRotation(40 * mTopCard.getTranslationX() / (getWidth() / 2.f));
mLastTouchX = x;
mLastTouchY = y;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsRemovedNoFling = false;
if (mIsFlingAnimating) {
return true;
}
if (!mDragging) {
return true;
}
mDragging = false;
mActivePointerId = INVALID_POINTER_ID;
ValueAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTopCard,
PropertyValuesHolder.ofFloat("translationX", 0),
PropertyValuesHolder.ofFloat("translationY", 0),
// PropertyValuesHolder.ofFloat("rotation", (float) Math.toDegrees(mRandom.nextGaussian() * DISORDERED_MAX_ROTATION_RADIANS)),
PropertyValuesHolder.ofFloat("rotation", 0),
PropertyValuesHolder.ofFloat("pivotX", mTopCard.getWidth() / 2.f),
PropertyValuesHolder.ofFloat("pivotY", mTopCard.getHeight() / 2.f),
PropertyValuesHolder.ofFloat("alpha", 1.0f)
).setDuration(250);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
break;
case MotionEvent.ACTION_POINTER_UP:
mIsRemovedNoFling = false;
if (mIsFlingAnimating) {
return true;
}
pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = event.getX(newPointerIndex);
mLastTouchY = event.getY(newPointerIndex);
mActivePointerId = event.getPointerId(newPointerIndex);
}
break;
}
return true;
}
private boolean removeTopCardRotation() {
float rot = Math.abs(mTopCard.getRotation());
if (rot > mMaxRotation / 2) {
float normalize = (rot / mMaxRotation) * 2 - 1;
mTopCard.setAlpha(1 - normalize);
}
if (rot > mMaxRotation) {
removeTopCard();
return true;
}
return false;
}
@Override
public View getSelectedView() {
throw new UnsupportedOperationException();
}
@Override
public void setSelection(int position) {
throw new UnsupportedOperationException();
}
public int getGravity() {
return mGravity;
}
public void setGravity(int gravity) {
mGravity = gravity;
}
public static class LayoutParams extends ViewGroup.LayoutParams {
int viewType;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(int w, int h, int viewType) {
super(w, h);
this.viewType = viewType;
}
}
private class GestureListener extends SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.d("Fling", "Fling with " + velocityX + ", " + velocityY);
if (Math.abs(velocityX) > mFlingSlop * MainActivity.maxFlingSensitivity
&& !mIsRemovedNoFling) {
removeTopCard();
return true;
} else
return false;
}
}
private void removeTopCard() {
if (!mIsFlingAnimating) {
// Lock animator incase fling again while animating
mIsFlingAnimating = true;
float targetX = mTopCard.getX();
// Choose animation side
if (targetX <= 0) {
// Left
targetX = -getWidth() / 4;
} else {
// Right
targetX = getWidth() / 4;
}
targetX *= 3;
// Animation Durations
final long duration = 500;
// Layout removal requires final
final float finalTargetX = targetX;
final View topCard = mTopCard;
topCard.animate()
.setDuration(duration)
.alpha(.00f)
.setInterpolator(new LinearInterpolator())
.x(targetX)
.rotation(Math.copySign(mMaxRotation, targetX))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Remove top card
removeViewInLayout(topCard);
// Repopulate adapter
ensureFull();
// Unlock animator
mIsFlingAnimating = false;
// Increment start index
++mAdapterStartIndex;
}
@Override
public void onAnimationCancel(Animator animation) {
onAnimationEnd(animation);
}
@Override
public void onAnimationStart(Animator animation) {
// Report listener
CardModel cardModel = getTopCardModel();
if (cardModel.getOnCardDimissedListener() != null) {
if (finalTargetX > 0) {
cardModel.getOnCardDimissedListener().onLike();
} else {
cardModel.getOnCardDimissedListener().onDislike();
}
}
// Reference next top card
mTopCard = getChildAt(getChildCount() - 2);
if (mTopCard != null) {
mTopCard.setLayerType(LAYER_TYPE_HARDWARE, null);
// Straighten next card
mTopCard.animate()
.rotation(0)
.setDuration(duration);
// Add url listener to top card
addUrlListener(mTopCard);
}
}
});
}
}
public void refreshTopCard() {
ensureFull();
if (getChildCount() != 0) {
mTopCard = getChildAt(getChildCount() - 1);
if (mTopCard != null)
mTopCard.setLayerType(LAYER_TYPE_HARDWARE, null);
mNumberOfCards = getAdapter().getCount();
}
}
public void addUrlListener(final View view) {
final SimpleDraweeView companyImage = ((SimpleDraweeView) view.findViewById(R.id.image));
companyImage.setOnTouchListener(new View.OnTouchListener() {
private Rect rect;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
companyImage.setColorFilter(Color.argb(50, 0, 0, 0));
rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
mIsUrlPressedDown = true;
break;
case MotionEvent.ACTION_UP:
if (rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) {
companyImage.setColorFilter(Color.argb(0, 0, 0, 0));
mIsUrlPressedDown = false;
WebView webView = (WebView) view.findViewById(R.id.web);
WebSettings settings = webView.getSettings();
settings.setSupportZoom(true);
settings.setBuiltInZoomControls(true);
AutofitTextView textView = (AutofitTextView) view.findViewById(R.id.description);
if (webView.getVisibility() == GONE) {
textView.setVisibility(GONE);
if (webView.getUrl() == null) {
webView.setWebViewClient(new WebViewClient());
webView.loadUrl("http://www.careercup.com" + getTopCardModel().getId());
}
webView.setVisibility(VISIBLE);
} else {
textView.setVisibility(VISIBLE);
webView.setVisibility(GONE);
}
}
break;
case MotionEvent.ACTION_MOVE:
if (!rect.contains(v.getLeft() + (int) event.getX(), v.getTop() + (int) event.getY())) {
companyImage.setColorFilter(Color.argb(0, 0, 0, 0));
mIsUrlPressedDown = false;
}
}
return true;
}
});
}
public CardModel getTopCardModel() {
CardModel cardModel = (CardModel) getAdapter().getItem(mAdapterStartIndex);
return cardModel;
}
}